package cn.edu.fzu.homemaking.http;

import static java.nio.charset.CodingErrorAction.IGNORE;

import java.io.IOException;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.net.ssl.SSLContext;

import org.apache.commons.collections.MapUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.RequestAcceptEncoding;
import org.apache.http.client.protocol.ResponseContentEncoding;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;

/**
 * 基于 apache httpclient 的 http 客户端
 */
final class ApacheHttpClient extends AbstractHttpClient {

    private static final Logger       log = LoggerFactory.getLogger(ApacheHttpClient.class);

    private final RequestConfig       requestConfig;
    private final CloseableHttpClient httpClient;

    ApacheHttpClient(int connectTimeout, int readTimeout, Charset charset) {
        this.charset = charset;

        this.requestConfig = RequestConfig.custom().setConnectionRequestTimeout(connectTimeout)
                .setConnectTimeout(connectTimeout).setSocketTimeout(readTimeout).setExpectContinueEnabled(false)
                .build();

        final SSLContext sslctx;
        try {
            sslctx = SSLContexts.custom().setProtocol(SSLConnectionSocketFactory.TLS)
                    .loadTrustMaterial(KeyStore.getInstance(KeyStore.getDefaultType()), TrustAllStrategy.INSTANCE)
                    .build();
        } catch (Exception e) {
            throw new UnsupportedOperationException(e);
        }

        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .register("https", new SSLConnectionSocketFactory(sslctx, NoopHostnameVerifier.INSTANCE)).build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        connectionManager.setMaxTotal(2048);
        connectionManager.setDefaultMaxPerRoute(1024);
        connectionManager.setDefaultSocketConfig(SocketConfig.custom()
                // 读超时
                .setSoTimeout(readTimeout).setTcpNoDelay(true).setSoKeepAlive(true).setSoLinger(0).build());
        connectionManager.setDefaultConnectionConfig(ConnectionConfig.custom().setCharset(charset)
                // 忽略
                .setMalformedInputAction(IGNORE).setUnmappableInputAction(IGNORE).build());

        this.httpClient = HttpClients.custom()
                // 不允许自动重试 
                .disableAutomaticRetries().setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                // 启用 gzip 压缩
                .addInterceptorLast(new RequestAcceptEncoding()).addInterceptorLast(new ResponseContentEncoding())
                .build();
    }


    @Nullable
    @Override
    protected byte[] execute(boolean get, String url, Map<String, String> headers, Map<String, String> parameters,
                             @Nullable byte[] content)
            throws IOException {

        HttpRequestBase req = get ? new HttpGet(join(parameters, url)) : assemblePost(url, parameters, content);
        req.setConfig(requestConfig);

        if (MapUtils.isNotEmpty(headers)) {
            for (Entry<String, String> item : headers.entrySet()) {
                req.setHeader(item.getKey(), item.getValue());
            }
        }

        try (CloseableHttpResponse response = httpClient.execute(req)) {
            HttpEntity entity = response.getEntity();
            try {
                if (Thread.currentThread().isInterrupted()) {
                    return null;
                }

                StatusLine statusLine = response.getStatusLine();
                int statusCode = statusLine.getStatusCode();
                if (statusCode >= 200 && statusCode < 300) {
                    return entity == null ? EMPTY_BYTES : EntityUtils.toByteArray(entity);
                } else {
                    log.error("httpfail: status={}, reason={}, url={}", statusCode, statusLine.getReasonPhrase(), url);
                    return null;
                }
            } finally {
                EntityUtils.consume(entity);
            }
        }
    }


    private HttpPost assemblePost(String url, Map<String, String> parameters, @Nullable byte[] content) {
        HttpPost req = new HttpPost(url);
        if (content != null) {
            req.setEntity(new ByteArrayEntity(content));
        } else if (MapUtils.isNotEmpty(parameters)) {
            List<NameValuePair> nvps = new ArrayList<>(parameters.size());
            for (Entry<String, String> entry : parameters.entrySet()) {
                nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
            req.setEntity(new UrlEncodedFormEntity(nvps, charset));
        }
        return req;
    }


    /** 仅在应用退出时调用此方法，例如 shutdownhook 或者 web 应用的 listener **/
    public void close() {
        try {
            httpClient.close();
        } catch (IOException e) {
            // do nothing
        }
    }

}
